package com.ian.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;

import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.H2;
import static org.springframework.security.config.Customizer.withDefaults;

import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.access.intercept.RunAsImplAuthenticationProvider;
import org.springframework.security.access.intercept.RunAsManagerImpl;
import org.springframework.security.access.vote.RoleHierarchyVoter;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.util.Collections;
import java.util.Map;

/**
 * Spring Security Config
 *
 * @author Witt
 * @version 1.0.0
 * @date 2022/4/30
 */

@EnableWebSecurity
/*
 * @EnableGlobalMethodSecurity 启用全局方法安全功能
 *   securedEnabled = true：启用注解 @Secured
 *   prePostEnabled = true：启用注解 @PreAuthorize(), @PostAuthorize(), @PreFilter(), @PostFilter()
 *   jsr250Enabled = true：启用JSR-250. These are standards-based and allow simple role-based constraints to be applied but do not have the power Spring Security’s native annotations.
 * note1: The annotated methods will only be secured for instances which are defined as Spring beans (in the same application context in which method-security is enabled). If you want to secure instances which are not created by Spring (using the new operator, for example) then you need to use AspectJ.
 * note2: You can enable more than one type of annotation in the same application, but only one type should be used for any interface or class as the behaviour will not be well-defined otherwise. If two annotations are found which apply to a particular method, then only one of them will be applied.
 */
// 启用注解 @PreAuthorize(), @PostAuthorize(), @PreFilter(), @PostFilter()
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true)
//@EnableMethodSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeRequests()
                .mvcMatchers("/resources/**", "/signup", "/about").permitAll()
//                .mvcMatchers("/admin/**").hasRole("ADMIN")
                .mvcMatchers("/db/**")
                // .not() // 效果就是权限属性前加上 “非” 符号，attribute = "!" + attribute; 即取反，使用和当前访问权限相反的权限
                // hasRole('ADMIN') 和 hasAuthority('ADMIN') 的唯一区别：hasRole('ADMIN')的参数 ADMIN 前面会自动补上前缀，默认是 ROLE_，然后再判断；
                //   而 hasAuthority('ADMIN') 直接判断；
                //   但是，hasPermission('ADMIN') 和上面的两者完全不同，需要自己写一些实现类来自定义判断逻辑
                .access("hasRole('ADMIN') and hasRole('DBA') and (hasAuthority('ADMIN') or isAnonymous() or isAuthenticated() or isFullyAuthenticated() or isRememberMe() or permitAll() or hasIpAddress('192.168.1.0/24') or @webSecurity.check(authentication,request))")
                .antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication, #userId)")
//                .anyRequest().denyAll()
                // permitAll()和anonymous()的区别：anonymous() 只允许匿名用户访问,不允许已登录用户访问；permitAll() 不管登录，不登录，都能访问
                .antMatchers("/hello1").anonymous()
                .antMatchers("/hello2", "/invalidSessionStrategy.html",
                        "invalidSessionUrl.html", "/logout", "/login?logout").permitAll()
//                 .antMatchers("/**").permitAll()
                .antMatchers(HttpMethod.GET, "/user").hasRole("user")
                .antMatchers(HttpMethod.GET, "/admin/**").hasRole("admin")
                .anyRequest().authenticated()
                // ObjectPostProcessor: to modify or replace many of the Object instances created by the Java Configuration
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O fsi) {
                        fsi.setPublishAuthorizationSuccess(true);
                        return fsi;
                    }
                })
                .and()
                /*
                 formLogin()：
                   无参，则返回 FormLoginConfigurer<HttpSecurity>，并且后面可以跟一连串的配置项；
                   有参，则返回 HttpSecurity，所有的配置项都在参数的lambda表达式中；
                   两者作用相同。
                   eg. 无参
                       .formLogin()
                       .loginPage("/login")
                       .permitAll()
                       .and()
                   eg. 使用默认配置，但即使使用默认配置，也必须要有该formLogin()配置
                       .formLogin(Customizer.withDefaults())
                 */
                // loginPage("/login")：映射到Controller中的方法 /login，再由该方法跳转到具体的 html页面
                .formLogin(form -> form.loginPage("/login").loginProcessingUrl("/doLogin").permitAll())
                // 报错：java.lang.IllegalArgumentException: '../templates/login.html?error' is not a valid redirect URL
                // 直接跳转到具体的 html页面，但是无法识别 templates 目录下的文件（因为包含thymeleaf语法的html文件，必须在 templates 目录下，语法才能生效），
                //   解决方法是在application.yml中加了 static-locations 配置。但是还是不行，好像语法还是不生效，必须通过controller跳转。
                // .formLogin(form -> form.loginPage("/login.html").loginProcessingUrl("/doLogin").permitAll())
                // .httpBasic(Customizer.withDefaults())
                /*
                .sessionManagement(): 和 formLogin() 类似，可以无参，也可有参
                eg. 有参：
                    .sessionManagement(sessionManagementCOnfigurer ->
                        sessionManagementCOnfigurer.invalidSessionUrl("/invalidSessionUrl.html")
                                .maximumSessions(1)
                                .maxSessionsPreventsLogin(false))
                 */
                .sessionManagement()
                // .sessionFixation().none()：session失效，在重新登录后，也不改变session；
                // .sessionFixation().migrateSession()、.sessionFixation().changeSessionId()、.sessionFixation().newSession()：session失效，在重新登录后，改变session；
                .sessionFixation().migrateSession()
                .maximumSessions(1)
                // 超过允许同时登录的最大session数时，是否拒绝登录
                //   默认为 false，该情况下，第二次登录，会令第一次的登录session强制失效，也就是说，只能一方登录，允许同时登录的最大session数为1
                //   如果为 true，则超过允许同时登录的最大session数时，直接拒绝登录
                .maxSessionsPreventsLogin(false)

                // invalidSessionUrl() 是 invalidSessionStrategy() 的 快捷方式设置；当设置了 invalidSessionStrategy() ，则 invalidSessionUrl()设置失效
                // .invalidSessionUrl("/invalidSessionUrl.html")
                // .invalidSessionStrategy(new SimpleRedirectInvalidSessionStrategy("/invalidSessionStrategy.html"))

                .and().and()
                // rememberMe(): 退出浏览器，下次再次打开浏览器的该应用程序，仍然可以继续访问，而不用再次登录，当然“remmeber-me”功能有过期时间，默认2周
                //   注意：要使用 rememberMe 功能，必须要重写父类的 userDetailsService()，否则会报错：userDetailsService is required...
                .rememberMe(rememberMe -> rememberMe
                        .key("rememberMeKey")
                        // 记住我 功能的用户校验
                        // 这个不设置也行，因为是默认值，不设置也会使用该值
                        .userDetailsService(userDetailsService()))
                .logout()
                .and()
                .logout(logout -> logout
                        .logoutUrl("/logout") // default
                        .logoutSuccessUrl("/login?logout") // default
                        // If this is specified, logoutSuccessUrl() is ignored
                        // .logoutSuccessHandler(logoutSuccessHandler)
                        .invalidateHttpSession(true) // default
                        .deleteCookies("JSESSIONID")
                        // .addLogoutHandler(logoutHandler)
                        .permitAll()
                )
//                .and()
                .csrf().disable() // default: enabled
        ;
    }


    /**
     * 1 In memory
     *
     * @return UserDetailsService类型的对象
     @Bean public UserDetailsService userDetailsService() {
     // 1 In memory: method 1
     UserDetails user = User.builder()
     .username("user")
     // 密码明文是 password
     .password("{bcrypt}$2a$10$lmURdwCsjA9jFc5NgzX1Keq5O91WEuOOgEtMQ246X.G/cu.HSqMTG")
     .roles("USER")
     .build();
     UserDetails admin = User.builder()
     .username("admin")
     // 密码明文是 password
     .password("{bcrypt}$2a$10$lmURdwCsjA9jFc5NgzX1Keq5O91WEuOOgEtMQ246X.G/cu.HSqMTG")
     .roles("ADMIN")
     .build();

     // 2 In memory: method 2
     //        UserDetails user = User.withDefaultPasswordEncoder()
     //                .username("user2")
     //                .password("password")
     //                .roles("USER")
     //                .build();
     //        UserDetails admin = User.withDefaultPasswordEncoder()
     //                .username("admin2")
     //                .password("password")
     //                .roles("ADMIN")
     //                .build();

     return new InMemoryUserDetailsManager(user, admin);
     }
     */

    /**
     * 3 JDBC
     *
     * @return UserDetailsManager类型的对象，UserDetailsManager接口，其继承了UserDetailsService接口
     */
    @Override
    @Bean
    protected UserDetailsService userDetailsService() {
        UserDetails user = User.builder()
                .username("user3")
                // 当显式设置了 PasswordEncoder 的 Bean，则此处的password()方法中的密码参数字符串不需要再加前缀 {bcrypt}；
                //     因为，比如设置了 PasswordEncoder 的 实现类为 BCryptPasswordEncoder，则它直接对密码密文进行解码，而不需要再去读取密文前缀（如{bcrypt}）
                //   否则需要 .password("{bcrypt}$2a$10$lmURdwCsjA9jFc5NgzX1Keq5O91WEuOOgEtMQ246X.G/cu.HSqMTG")
                //     因为，此时的 PasswordEncoder 的 实现类为 DelegatingPasswordEncoder（默认值），
                //     它在对密码密文进行解密前，需要先读取密文的前缀（如{bcrypt}），然后根据前缀选择针对性的 PasswordEncoder 实现类进行具体的解密工作
                //   密码明文是 password
                .password("$2a$10$lmURdwCsjA9jFc5NgzX1Keq5O91WEuOOgEtMQ246X.G/cu.HSqMTG")
                .roles("user")
                .build();
        UserDetails admin = User.builder()
                .username("admin3")
                //   密码明文是 password
                .password("$2a$10$lmURdwCsjA9jFc5NgzX1Keq5O91WEuOOgEtMQ246X.G/cu.HSqMTG")
                .roles("admin")
                .build();
        // spring容器中有一个 DataSource 的 Bean（下面的 dataSource() 方法注入的），此处自动注入到该参数中了
        JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource());
        jdbcUserDetailsManager.createUser(user);
        jdbcUserDetailsManager.createUser(admin);
        return jdbcUserDetailsManager;
    }

    @Bean
    DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(H2)
                .setScriptEncoding("UTF-8")
                .ignoreFailedDrops(true)
                .addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl")
                .build();
    }

    /**
     * Customizing the PasswordEncoder implementation used by Spring Security can be done by exposing a PasswordEncoder Bean.
     *
     * @return
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        // return NoOpPasswordEncoder.getInstance(); // NoOpPasswordEncoder 直接存储密码的明文
        return new BCryptPasswordEncoder(); // 默认密码处理方式，BCryptPasswordEncoder 对密码利用 BCrypt 算法编码后存储
    }

    /**
     * Spring容器中注入 HttpSessionEventPublisher bean
     *
     * @return
     */
    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

    /**
     * Spring容器中注入 AuthenticationEventPublisher bean: bean1
     * Then, you can use Spring’s @EventListener support
     * @return AuthenticationEventPublisher
     @Bean public AuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
     return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
     }
     */

    /**
     * Spring容器中注入 AuthenticationEventPublisher bean: bean2
     * Then, you can use Spring’s @EventListener support
     * @return AuthenticationEventPublisher
     @Bean public AuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
     Map<Class<? extends AuthenticationException>, Class<? extends AbstractAuthenticationFailureEvent>> mapping =
     Collections.singletonMap(FooException.class, FooEvent.class);
     DefaultAuthenticationEventPublisher defualtAuthenticationEventPublisher = new DefaultAuthenticationEventPublisher(applicationEventPublisher);
     defualtAuthenticationEventPublisher.setAdditionalExceptionMappings(mapping);
     return defualtAuthenticationEventPublisher;
     }
     */

    /**
     * Spring容器中注入 AuthenticationEventPublisher bean: bean3
     * Then, you can use Spring’s @EventListener support
     *
     * @return AuthenticationEventPublisher
     */
    @Bean
    public AuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        DefaultAuthenticationEventPublisher defaultAuthenticationEventPublisher = new DefaultAuthenticationEventPublisher(applicationEventPublisher);
        // supply a catch-all event to fire in the case of any AuthenticationException
        // defaultAuthenticationEventPublisher.setDefaultAuthenticationFailureEvent(GenericAuthenticationFailureEvent.class);
        defaultAuthenticationEventPublisher.setDefaultAuthenticationFailureEvent(AbstractAuthenticationFailureEvent.class);
        return defaultAuthenticationEventPublisher;
    }

    class FooException extends AuthenticationException {
        public FooException(String msg) {
            super(msg);
        }
    }

    class FooEvent extends AbstractAuthenticationFailureEvent {
        public FooEvent(Authentication authentication, AuthenticationException exception) {
            super(authentication, exception);
        }
    }

    //    @Component
    class WebSecurity {
        public boolean check(Authentication authentication, HttpServletRequest request) {
            return true;
        }

        public boolean checkUserId(Authentication authentication, int id) {
            if (id > 5) {
                return true;
            } else {
                return false;
            }
        }
    }

    @Bean // 该bean的名字的默认值为 方法名称 webSecurity
    public WebSecurity webSecurity() {
        return new WebSecurity();
    }

    /**
     * Spring容器中注入 Bean：runAsManager
     * @return RunAsManagerImpl
     @Bean // 该 Bean 的名称，默认为 被注解的方法名
     public RunAsManager runAsManager() {
     RunAsManagerImpl runAsManagerImpl = new RunAsManagerImpl();
     runAsManagerImpl.setKey("my_run_as_password");
     return runAsManagerImpl;
     }
     */

    /**
     * Spring容器中注入 Bean：runAsAuthenticationProvider
     * 注入该 bean 后，会导致 无法正常登录，总是提示 Invalid username and password
     *
     * @return RunAsImplAuthenticationProvider
     @Bean public RunAsImplAuthenticationProvider runAsAuthenticationProvider() {
     RunAsImplAuthenticationProvider runAsImplAuthenticationProvider = new RunAsImplAuthenticationProvider();
     runAsImplAuthenticationProvider.setKey("my_run_as_password");
     return runAsImplAuthenticationProvider;
     }
     */

    /**
     * configure which roles (or authorities) should include others
     * method 1   无效；另外，此处返回的 AccessDecisionVoter 已经过时了，不建议使用
     * @return AccessDecisionVoter
     @Bean AccessDecisionVoter hierarchyVoter() {
     RoleHierarchyImpl roleHierarchyImpl = new RoleHierarchyImpl();
     // The > symbol can be thought of as meaning "includes"
     roleHierarchyImpl.setHierarchy("ROLE_admin > ROLE_staff\n" +
     "ROLE_staff > ROLE_user\n" +
     "ROLE_user > ROLE_guest");
     return new RoleHierarchyVoter(roleHierarchyImpl);
     }
     */

    /**
     * configure which roles (or authorities) should include others
     * method 2   有效
     *
     * @return RoleHierarchy
     */
    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchyImpl = new RoleHierarchyImpl();
//        roleHierarchyImpl.setHierarchy("ROLE_admin > ROLE_user");
        roleHierarchyImpl.setHierarchy("ROLE_admin > ROLE_staff\n" +
                "ROLE_staff > ROLE_user\n" +
                "ROLE_user > ROLE_guest");
        return roleHierarchyImpl;
    }

}
